/*
 * This class is the clients runable class.
 * 
 * Contains the following methods:
 * + FlopBoxClient()
 * + fileChanged()
 * + startFolderWatcher()
 * + stopFolderWatcher()
 * + startServerSync()
 * - uploadFile()
 * - isFileReady()
 * - canOpenFile()
 * - printDirectory()
 * - refresh()
 * - delete()
 * - uploadFile()
 * 
 * And a private class, ThreadSyncer, which has the following methods
 * + ThreadSyncer()
 * + run()
 */
package com.flopbox.client;

import com.flopbox.server.FileInfo;
import com.flopbox.server.RemoteFlopboxServer;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import static java.nio.file.StandardWatchEventKinds.*;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

public class FlopBoxClient implements FolderListener {

    // Static variables
    public static String HOST = "158.38.43.32";
    public static String NAME = "flopBoxServer";
    public static int PORT = 1337;
    public static int INTERVAL = 10;
    public static String LOCATION = System.getProperty("user.home") + File.separator + "flopBox";
    // Objects
    private Watcher mWatcher;
    private RemoteFlopboxServer mStub;
    private ServerSyncer mSyncer;
    private FileHandler mHandler;
    
    // Session array, to avoid uploading a recently downloaded file
    private ArrayList<FileInfo> sessionList = new ArrayList<>();
    
    
    private final static Logger LOGGER = Logger.getLogger(FlopBoxClient.class.getName());

    /**
     * This method initiate the class, and it does the following 1. Set the
     * property of the host address 2. Trying to connect to the given server 3.
     * Getting the users home directory path 3.1 Checking if this directory
     * actually exists 4. initiate the directory watcher, mWatcher 5. initiating
     * the directory listener for mWatcher 6. initiating a new sync object
     *
     * @throws Exception
     */
    public FlopBoxClient() throws Exception {
        System.setProperty("java.rmi.server.hostname", HOST);

        // Try to set up connection to Server
        final String bindName = "rmi://" + HOST + ':' + PORT + '/' + NAME;

        mStub = (RemoteFlopboxServer) Naming.lookup(bindName);
        System.out.println("Connected to server");

        // register directory and process its events
        Path dir = Paths.get(LOCATION);

        // Check if path exists
        if (!dir.toFile().isDirectory()) {
            // Create path
            File f = dir.toFile();
            f.mkdir();
        }

        // Create mWatcher object
        mWatcher = new Watcher(dir);

        // Create sync object
        mSyncer = new ServerSyncer(INTERVAL);
        
        // Initiate mHandler
        mHandler = new FileHandler();
    }
    
    public void initiateWatcher(){
        mWatcher.addFolderListener(this);
    }

    /**
     * When something in the given folder is changed, this method is initialized
     * it then checks if the change is a new file created. If that is the case,
     * it initiate a upload of this new file
     *
     * @param kind
     * @see java.nio.file.WatchEvent
     * @param child
     * @see java.nio.file.Path
     */
    @Override
    public void fileChanged(Kind kind, Path child) {        
        if (kind == ENTRY_CREATE || kind == ENTRY_MODIFY) {                        
            mHandler.uploadFile(child);
        } else if (kind == ENTRY_DELETE) {
            mHandler.deleteRemote(child);
        }
    }
    // Private class FileHandler start
    private class FileHandler {

        /**
         *
         * @param p - Path to the file that should be deleted
         */
        private void deleteLocal(Path p) {
            String stringPath = p.toString();
            File f = new File(stringPath);
            if (f.delete()) {
                log("delete(): File " + stringPath + " deleted");
            } else {
                log("delete(): File " + stringPath + " could not be deleted");
            }
        }

        
        private boolean deleteRemote(Path p) {
            FileInfo f = new FileInfo(new Date(), (p.toFile()).getName(), true);
            try {
                // Try to tell the server to delete a File
                int id = mStub.initUpload(f);

                // 
                if (id == RemoteFlopboxServer.DELETE_ACKNOWLEDGE) {
                    return true;
                }
            } catch (RemoteException ex) {
                log("Couldn't initialize upload");
            } finally {
                return false;
            }
        }

        /**
         * UploadFile. This method initiate the upload process to the server. It
         * then sends the file as a stream of bytes until the file is sent
         *
         * @param child
         */
        public void uploadFile(Path p) {
            // Check if the newly created file came from the server
            for (FileInfo f : sessionList){
                if (f.getFilename().equals((p.toFile()).getName())){
                    
                    // Stop the method if the filenames are the same
                    return;
                }
            }
            
            try {
                mStub.sendString("Ready to upload files");
                File f = new File(p.toString());
                System.out.println("Value of isFileReady() :"+isFileReady(f));
                if (!f.getName().equalsIgnoreCase(".DS_Store") && isFileReady(f)) {
                    System.out.println("Can write");
                    int id = mStub.initUpload(new FileInfo(new Date(), p.getFileName().toString()));
                    System.out.println("uploadFile(): upload id recieved from server: "+id);
                    if (id > 0) {
                        FileInputStream fis = new FileInputStream(p.toFile());
                        byte[] buf = new byte[1024];

                        while (isFileReady(f) && fis.read(buf) != -1) {
                            mStub.uploadFile(id, buf);
                        }

                        fis.close();
                        mStub.doneUploadingFile(id);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        public void downloadFile(FileInfo fi) {
            // Store the new file to the sessionList
            sessionList.add(fi);
            
            try {
                // Get our download ID from the server
                int id = mStub.initDownload(fi);

                // Check if the id is un-equal to TRANSFER_DENIED, i.e.
                // that we're allowed to download the file
                if (id != RemoteFlopboxServer.TRANSFER_DENIED) {
                    log("Recieved a positive ID: " + id);
                    // Create a File object to store data from the server
                    File newFile = new File(LOCATION, fi.getFilename());
                    FileOutputStream fos = new FileOutputStream(newFile);
                    DataOutputStream dos = new DataOutputStream(fos);

                    log("Ready to download file");
                    
                    // Fetch data from the server and store right away
                    byte[] ar;
                    while ((ar = mStub.downloadFile(id)).length > 0) {
                        dos.write(ar);
                        dos.flush();
                        
                        if (ar.length < 1024){
                            break;                            
                        }
                        
                    }
                    
                    // To avoid recursive uploading of downloaded files
                    newFile.setLastModified(fi.getModified().getTime());
                    
                    sessionList.remove(fi);
                    
                    log("Done downloading");
                }

            } catch (Exception e) {
                log("Failed to download file");
            }
        }

        /**
         * Loops until file is ready. This method checks if the file is locked
         * by the OS, ie. used by someone else.
         *
         * @param f - file to check
         * @see java.io.File
         * @return True, if the file is available
         */
        private boolean isFileReady(File f) {
            int sleepTime = 1000;
            while (!canOpenFile(f)) {
                System.out.println("Can't open file!");
                try {
                    Thread.sleep(sleepTime);
                    sleepTime *= 2;
                } catch (InterruptedException ex) {
                    Logger.getLogger(FlopBoxClient.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            return true;
        }

        /**
         * Method to check if the file can be opened for transmitting. This
         * method tries to open a file with. If that succeeds true is returned.
         * If it don't succeed, false is returned. This happens if the file is
         * locked.
         *
         * @param f
         * @see java.io.File
         * @return boolean
         */
        private boolean canOpenFile(File f) {
            boolean canOpen = false;
            try (FileInputStream fin = new FileInputStream(f)) {
                canOpen = true;
            } catch (Exception e) {
                e.printStackTrace();
                canOpen = false;
            } finally {
                return canOpen;
            }
        }
    }
    //End private Class FileFileHandler

    /**
     * Method to initiate mWatcher
     *
     */
    public void startFolderWatcher() {
        mWatcher.start();
    }

    /**
     * Method to stop mWatcher
     *
     */
    public void stopFolderWatcher() {
        if (mWatcher.isAlive()) {
            try {
                mWatcher.join();
            } catch (InterruptedException ex) {
                Logger.getLogger(FlopBoxClient.class
                        .getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    public void refresh() {
        try {
            ArrayList<FileInfo> myFiles = mWatcher.listFiles();
            //Retrives a list of modified files from server
            List<FileInfo> serverFiles = mStub.getFilesModified(myFiles);
            
            log("List of files from server of length " + serverFiles.size());
            for (FileInfo sf : serverFiles) {
                // Checks if the file exists on client side
                int index = java.util.Collections.binarySearch(myFiles, sf);
                if (index >= 0) {
                    FileInfo myFile = myFiles.get(index);

                    // Constructs the filesystem specific Path to local file:
                    Path path = FileSystems.getDefault().getPath(mWatcher.getDir(), myFile.getFilename());
                    log("Path: " + path.toString());
                    
                    // If modified date for the local file is after the one on the server,
                    // upload the local file
                    if (myFile.getModified().equals(sf.getModified()) 
                        || myFile.getModified().after(sf.getModified())) {
                        // Upload file myFile
                        mHandler.uploadFile(path);
                        // If the local file is older...
                    } else if (myFile.getModified().before(sf.getModified())) {
                        // ... check if the file is deleted on server
                        if (sf.isDeleted()) {
                            // Delete file on client side
                            mHandler.deleteLocal(path);
                        } //Updates file on client side
                        
                    }
                }
                else {
                    //Update file on client side
                    log("refresh(): calling downloadFile()");
                    mHandler.downloadFile(sf);
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    private class ServerSyncer extends Thread {

        private int interval;

        public ServerSyncer(int interval) {
            this.interval = interval;
        }

        public void setInterval(int interval) {
            this.interval = interval;
        }

        @Override
        public void run() {
            // Set up a timer
            long start = System.currentTimeMillis();
            long stop;
            while (true) {
                stop = System.currentTimeMillis();
                int secondsWaited = (int) (stop - start) / 1000;

                // Check if we have waited longer than the interval
                if (secondsWaited > interval) {
                    start = System.currentTimeMillis();

                    refresh();
                } else {
                    try {
                        Thread.sleep((interval - secondsWaited));
                    } catch (Exception e) {
                        // Do nothing
                    }
                }
            }
        }
    }

    /**
     *
     */
    public void startServerSync() {
        if (!mSyncer.isAlive()) {
            mSyncer.start();
        } else {
            System.out.println("Server sync is already running");
        }
    }

    private void printDirectory() {
        ArrayList<FileInfo> metaFiles = mWatcher.listFiles();
        System.out.println("Current files in " + mWatcher.getDir() + ":");
        for (FileInfo fi : metaFiles) {
            System.out.println(fi.toString());
        }
    }

    //Funciton for logging
    private void log(String message) {
        LOGGER.log(Level.INFO, message);
    }

    /**
     *
     * @param args
     */
    public static void main(String[] args) {
        FlopBoxClient fbc = null;
        try {
            fbc = new FlopBoxClient();
            fbc.initiateWatcher();
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (fbc != null) {
            fbc.startFolderWatcher();
            fbc.startServerSync();
        }
    }
}